Python intro#

gdsfactory is written in python and requires some basic knowledge of python.

If you are new to python you can find many resources online

This notebook is for you to experiment with some common python patterns in gdsfactory

Classes#

Gdsfactory has already some pre-defined classes for you. The only class you may need to define is a Layermap. Which can be easily defined as a dataclass

All the other classes (Component, ComponentReference, Port …) are already available in gf.types

Classes are good for keeping state, which means that they store some information inside them (polygons, ports, references …)

[1]:
import gdsfactory as gf
gf.CONF.plotter = 'holoviews'

c = gf.Component(name='my_fist_class')
c.add_polygon([(-8, 6, 7, 9), (-6, 8, 17, 5)], layer=(1,0))
c.plot()
2022-03-01 18:38:03.624 | INFO     | gdsfactory.config:<module>:52 - Load '/home/runner/work/gdsfactory/gdsfactory/gdsfactory' 4.2.16
[1]:

Functions#

Functions have clear inputs and outputs, they usually accept some parameters (strings, floats, ints …) and return other parameters

[2]:
def double(x):
    return 2*x

x = 1.5
y = double(x)
print(y)
3.0

It’s also nice to add type annotations to your functions to clearly define what are the input/output types (string, int, float …)

[3]:
def double(x:float)->float:
    return 2*x

Factories#

A factory is a function that returns an object. In gdsfactory many functions return a Component object

[4]:
def bend(radius:float=5)->gf.types.Component:
    return gf.components.bend_euler(radius=radius)

component = bend(radius=10)

print(component)
component.plot()
bend_euler_2e0a66fe: uid 2, ports ['o1', 'o2'], aliases [], 1 polygons, 0 references
[4]:

Decorators#

gdsfactory has many functions, and we want to do some common operations for the ones that return a Component:

  • give a unique name (dependent on the input parameters) to a Component

  • validate input argumens based on type annotations

  • cache the Component that the function returns

For that you will see a @cell decorator on many component functions.

The validation functionality comes from the pydantic package and is available to you automatically when using the @cell decorator

[5]:
from pydantic import validate_arguments

@validate_arguments
def double(x:float)->float:
    return 2*x

x = 1.5
y = double(x)
print(y)
3.0

The validator decorator is equivalent to running

[6]:
def double(x:float)->float:
    return 2*x

double_with_validator = validate_arguments(double)
x = 1.5
y = double_with_validator(x)
print(y)
3.0

The cell decorator also leverages that validate arguments. So you should add type annotations to your component factories.

Lets try to create an error x and you will get a clear message the the function double does not work with strings

y = double('not_valid_number')

will raise a ValidationError

ValidationError: 0 validation error for Double
x
  value is not a valid float (type=type_error.float)

It will also cast the input type based on the type annotation. So if you pass an int it will convert it to float

[7]:
x = 1
y = double_with_validator(x)
print(y, type(x), type(y))
2.0 <class 'int'> <class 'float'>

List comprehensions#

You will also see some list comprehensions, which are common in python.

For example, you can write many loops in one line

[8]:
y = []
for x in range(3):
    y.append(double(x))

print(y)
[0, 2, 4]
[9]:
y = [double(x) for x in range(3)]  # much shorter and easier to read
print(y)
[0, 2, 4]

Functional programming#

Functional programming follows linux philosophy:

  • Write functions that do one thing and do it well.

  • Write functions to work together.

  • Write functions with clear inputs and outputs

partial#

gdsfactory.partial comes from the module functools.partial, which is available in the standard python library.

The following two functions are equivalent in functionality.

Notice how the second one is shorter, more readable and easier to maintain thanks to gf.partial

[10]:
def ring_sc(gap=0.3, **kwargs):
    return gf.components.ring_single(gap=gap, **kwargs)

ring_sc = gf.partial(gf.components.ring_single, gap=0.3)

As you customize more parameters, it’s more obvious than the second one is easier to maintain

[11]:
def ring_sc(gap=0.3, radius=10, **kwargs):
    return gf.components.ring_single(gap=gap, radius=radius, **kwargs)

ring_sc = gf.partial(gf.components.ring_single, gap=0.3, radius=10)

compose#

gf.compose combines two functions into one.

[12]:
ring_sc = gf.partial(gf.components.ring_single, radius=10)
add_gratings = gf.routing.add_fiber_array

ring_sc_gc  = gf.compose(add_gratings, ring_sc)
ring_sc_gc5 = ring_sc_gc(radius=5)
ring_sc_gc5.plot()
[12]:
[13]:
ring_sc_gc20 = ring_sc_gc(radius=20)
ring_sc_gc20.plot()
[13]:

This is equivalent and more readable than writing

[14]:
ring_sc_gc5 = add_gratings(ring_sc_gc(radius=5))
ring_sc_gc5.plot()
[14]:
[15]:
ring_sc_gc20 = add_gratings(ring_sc_gc(radius=20))
ring_sc_gc20.plot()
[15]:
[16]:
print(ring_sc_gc5)
ring_single_212e6ad7_ad_e539e971: uid 18, ports ['vertical_te_00', 'vertical_te_01'], aliases [], 0 polygons, 22 references

Ipython#

This jupyternotebook uses an Interactive Python Terminal (Ipython). So you can interact with the code.

For more details on Jupyter Notebooks, you can visit the Jupyter website.

The most common trick that you will see is that we use ?? to see the documentation of a function

[17]:
gf.components.coupler?

To see the source code of a function you can use ??

[18]:
gf.components.coupler??

To see which variables you have defined in the workspace you can type whos

[19]:
whos
Variable                Type                         Data/Info
--------------------------------------------------------------
add_gratings            function                     <function add_fiber_array at 0x7f69204c8ca0>
bend                    function                     <function bend at 0x7f6913995820>
c                       Component                    my_fist_class: uid 1, por<...> 1 polygons, 0 references
component               Component                    bend_euler_2e0a66fe: uid <...> 1 polygons, 0 references
double                  function                     <function double at 0x7f6913995430>
double_with_validator   cython_function_or_method    <cyfunction double at 0x7f6913970790>
gf                      module                       <module 'gdsfactory' from<...>/gdsfactory/__init__.py'>
ring_sc                 partial                      functools.partial(<functi<...>7f6915258c10>, radius=10)
ring_sc_gc              Compose                      Compose(<function add_fib<...>f6915258c10>, radius=10))
ring_sc_gc20            Component                    ring_single_d8d11220_ad_d<...>0 polygons, 22 references
ring_sc_gc5             Component                    ring_single_212e6ad7_ad_e<...>0 polygons, 22 references
validate_arguments      cython_function_or_method    <cyfunction validate_arguments at 0x7f692b635c70>
x                       int                          2
y                       list                         n=3

To time the execution time of a cell, you can add a %timeit on top of the cell

[20]:
%time

def hi():
    print('hi')

hi()
CPU times: user 2 µs, sys: 0 ns, total: 2 µs
Wall time: 5.48 µs
hi

For more Ipython tricks you can find many resources available online